package it.unibo.refolding.pico.stripchart;

import it.unibo.refolding.common.CommonThings;
import it.unibo.refolding.pico.PicoConstants;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.commons.lang3.builder.EqualsBuilder;
import org.apache.commons.lang3.builder.HashCodeBuilder;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;

import com.google.common.base.Preconditions;

public class FetchRefoldBreakParams {

  private static final Logger logger = Logger.getLogger(FetchRefoldBreakParams.class.getSimpleName());

  public final double fetchRampSizeM;
  public final double fetchSurfaceDelayS;
  public final double velocityMPerS;
  public final double refoldingApproachM;
  public final double refoldingApproachVelocityMPerS;
  public final double refoldingSurfaceDelayS;
  public final double breakRetractionM;

  private static final double PUSH_M = 25e-9;

  public FetchRefoldBreakParams(double fetchRampSizeM, double fetchSurfaceDelayS,
    double velocityMPerS, double refoldingApproachM, double refoldingApproachVelocityMPerS,
    double refoldingSurfaceDelayS, double breakRetractionM) {
    this.fetchRampSizeM = fetchRampSizeM;
    this.fetchSurfaceDelayS = fetchSurfaceDelayS;
    this.velocityMPerS = velocityMPerS;
    this.refoldingApproachM = refoldingApproachM;
    this.refoldingApproachVelocityMPerS = refoldingApproachVelocityMPerS;
    this.refoldingSurfaceDelayS = refoldingSurfaceDelayS;
    this.breakRetractionM = breakRetractionM;
  }

  public static final FetchRefoldBreakParams STANDARD = createStandard();
  public static final FetchRefoldBreakParams GB1_WAIT_LESS = createGb1WaitLess();
  public static final FetchRefoldBreakParams GB1_WAIT_MORE = createGb1WaitMore();
  public static final FetchRefoldBreakParams GB1_CLOSE_TO_SURFACE_WAIT_500MS = createGb1CloseToSurfaceWait500ms();
  // public static final FetchRefoldBreakParams
  // GB1_FAR_FROM_SURFACE_WAIT_LESS = createGb1FarFromSurfaceWaitLess();

  public static final FetchRefoldBreakParams DIMER_HOVER_70NM_WAIT_250MS = createDimerHover70nmWait250ms();
  public static final FetchRefoldBreakParams DIMER_HOVER_35NM_WAIT_10MS = createDimerHover35nmWait10ms();
  public static final FetchRefoldBreakParams DIMER_HOVER_45NM_WAIT_10MS = createDimerHover45nmWait10ms();
  public static final FetchRefoldBreakParams DIMER_HOVER_55NM_WAIT_10MS = createDimerHover55nmWait10ms();

  private static FetchRefoldBreakParams createDimerHover70nmWait250ms() {
    return createParams(265e-9, PicoConstants.DEAD_TIME_S, 170e-9, PicoConstants.MAX_VELOCITY_M_PER_S, 250e-3, 405e-9, 2000e-9);
  }

  private static final Pattern patternV1 = Pattern.compile("Fetch(\\d+)nm_Refolding(\\d+)nm_Hover(\\d+)ms_Break(\\d+)nm_(\\d+)nmPerS");
  private static final Pattern patternV2 = Pattern.compile(String.format(
    Locale.ROOT,
    "Fetch(%1$s)nm_Refolding(%1$s)nm_RefoldingApproachVelocity(%1$s)nmPerS_Hover(%1$s)ms_Break(%1$s)nm_Velocity(%1$s)nmPerS",
    CommonThings.FP_REGEX));
  private static final Pattern patternV3 = Pattern.compile(String.format(
    Locale.ROOT,
    "Fetch(%1$s)nm_FetchSurfaceDelay(%1$s)ms_Refolding(%1$s)nm_RefoldingApproachVelocity(%1$s)nmPerS_Hover(%1$s)ms_Break(%1$s)nm_Velocity(%1$s)nmPerS",
    CommonThings.FP_REGEX));

  public static FetchRefoldBreakParams parse(String s) {
    Matcher matcher = patternV3.matcher(s);
    if (matcher.matches()) {
      double[] a = parseDoubleGroups(matcher);
      // Division is more accurate because 1e9 is exactly representable, while
      // 1e-9 is not.
      return createParams(a[0] / 1e9, a[1] / 1e3, a[2] / 1e9, a[3] / 1e9, a[4] / 1e3, a[5] / 1e9, a[6] / 1e9);
    }
    matcher = patternV2.matcher(s);
    if (matcher.matches()) {
      double[] a = parseDoubleGroups(matcher);
      // Division is more accurate because 1e9 is exactly representable, while
      // 1e-9 is not.
      return createParams(a[0] / 1e9, PicoConstants.DEAD_TIME_S, a[1] / 1e9, a[2] / 1e9, a[3] / 1e3, a[4] / 1e9, a[5] / 1e9);
    }
    matcher = patternV1.matcher(s);
    if (matcher.matches()) {
      double[] a = parseDoubleGroups(matcher);
      // Division is more accurate because 1e9 is exactly representable, while
      // 1e-9 is not.
      return createParams(
        a[0] / 1e9, PicoConstants.DEAD_TIME_S, a[1] / 1e9, PicoConstants.MAX_VELOCITY_M_PER_S, a[2] / 1e3,
        a[3] / 1e9, a[4] / 1e9);
    }
    logger.log(Level.WARNING, "{0} matches neither {1}, {2} nor {3}.", new Object[] {
    s, patternV3, patternV2, patternV1});
    return null;
  }

  public static double[] parseDoubleGroups(Matcher matcher) {
    double[] a = new double[matcher.groupCount()];
    for (int i = a.length; --i >= 0;) {
      a[i] = Double.parseDouble(matcher.group(1 + i));
    }
    return a;
  }

  public static FetchRefoldBreakParams createParams(
    double fetchRampM, double fetchSurfaceDelayS, double refoldingApproachM,
    double refoldingApproachVelocityMPerS, double hoverDelayS, double refoldRetractionM, double velocityMPerS) {
    return new FetchRefoldBreakParams(
      fetchRampM, fetchSurfaceDelayS, velocityMPerS, refoldingApproachM,
      refoldingApproachVelocityMPerS, hoverDelayS, refoldRetractionM);
  }

  private static FetchRefoldBreakParams createDimerHover35nmWait10ms() {
    return createParams(265e-9, PicoConstants.DEAD_TIME_S, 205e-9, PicoConstants.MAX_VELOCITY_M_PER_S, 10e-3, 440e-9, 2000e-9);
  }

  private static FetchRefoldBreakParams createDimerHover45nmWait10ms() {
    return createParams(265e-9, PicoConstants.DEAD_TIME_S, 195e-9, PicoConstants.MAX_VELOCITY_M_PER_S, 10e-3, 430e-9, 2000e-9);
  }

  private static FetchRefoldBreakParams createDimerHover55nmWait10ms() {
    return createParams(265e-9, PicoConstants.DEAD_TIME_S, 185e-9, PicoConstants.MAX_VELOCITY_M_PER_S, 10e-3, 420e-9, 2000e-9);
  }

  private static FetchRefoldBreakParams createStandard() {
    double fetchRampSizeM = 200e-9;
    double fetchSurfaceDelayS = 20e-3;
    // 05021932.008.pico has problems with only 0.98.
    double velocityMPerS = 2000e-9 * 0.99;
    double refoldingApproachM = 110e-9;
    double refoldingSurfaceDelayS = 1055e-3;
    double breakRetractionM = 500e-9 - (fetchRampSizeM - refoldingApproachM);
    return new FetchRefoldBreakParams(
      fetchRampSizeM, fetchSurfaceDelayS, velocityMPerS, refoldingApproachM,
      PicoConstants.MAX_VELOCITY_M_PER_S, refoldingSurfaceDelayS, breakRetractionM);
  }

  private static FetchRefoldBreakParams createGb1CloseToSurfaceWait500ms() {
    // 460 ms in the GUI
    return createGb1WaitMore(41e-9, 500e-3);
  }

  private static FetchRefoldBreakParams createGb1WaitLess() {
    return createGb1WaitLess(41e-9, 23e-3);
  }

  private static FetchRefoldBreakParams createGb1WaitLess(double hoverM, double fetchSurfaceDelayS) {
    double refoldingApproachM = 85e-9;
    double fetchRampSizeM = refoldingApproachM + hoverM + PUSH_M;
    double velocityMPerS = 2000e-9 * 0.99;
    double refoldingSurfaceDelayS = 15e-3;
    double breakRetractionM = 500e-9 - (fetchRampSizeM - refoldingApproachM);
    return new FetchRefoldBreakParams(
      fetchRampSizeM, fetchSurfaceDelayS, velocityMPerS, refoldingApproachM,
      PicoConstants.MAX_VELOCITY_M_PER_S, refoldingSurfaceDelayS, breakRetractionM);
  }

  private static FetchRefoldBreakParams createGb1WaitMore() {
    double refoldingSurfaceDelayS = 120e-3; // 105 ms in UI !!!
    return createGb1WaitMore(65e-9, refoldingSurfaceDelayS);
  }

  private static FetchRefoldBreakParams createGb1WaitMore(
    double hoverM, double refoldingSurfaceDelayS) {
    double refoldingApproachM = 85e-9;
    double fetchRampSizeM = refoldingApproachM + hoverM + PUSH_M;
    double fetchSurfaceDelayS = 23e-3;
    double velocityMPerS = 2000e-9 * 0.99;
    double breakRetractionM = 500e-9 - (fetchRampSizeM - refoldingApproachM);
    return new FetchRefoldBreakParams(
      fetchRampSizeM, fetchSurfaceDelayS, velocityMPerS, refoldingApproachM,
      PicoConstants.MAX_VELOCITY_M_PER_S, refoldingSurfaceDelayS, breakRetractionM);
  }

  @Override
  public String toString() {
    return ReflectionToStringBuilder.toString(this, ToStringStyle.SHORT_PREFIX_STYLE);
  }

  public double expectedTimeS() {
    return 2 * (fetchRampSizeM / velocityMPerS) + fetchSurfaceDelayS + PicoConstants.DEAD_TIME_S
      + refoldingApproachM / refoldingApproachVelocityMPerS + refoldingSurfaceDelayS
      + breakRetractionM / velocityMPerS;
  }

  @Override
  public boolean equals(Object other) {
    return EqualsBuilder.reflectionEquals(this, other);
  }

  public boolean approxEquals(FetchRefoldBreakParams other, double maxRelativeError) {
    Field[] fields = FetchRefoldBreakParams.class.getDeclaredFields();
    for (Field field : fields) {
      if (!Modifier.isStatic(field.getModifiers())) {
        double x, y;
        try {
          x = field.getDouble(this);
          y = field.getDouble(other);
        } catch (IllegalAccessException e) {
          throw new RuntimeException(e.getMessage(), e);
        }
        double m = (x + y) / 2;
        double re = Math.max((x - m) / m, (y - m) / m);
        if (re > maxRelativeError) {
          logger.log(Level.INFO, "x={0} y={1} re={2}", new Object[] {x, y, re});
          return false;
        }
      }
    }
    return true;
  }

  @Override
  public int hashCode() {
    return HashCodeBuilder.reflectionHashCode(this);
  }

  public static FetchRefoldBreakParams parseOrFail(String s) {
    FetchRefoldBreakParams x = parse(s);
    Preconditions.checkNotNull(x, "Could not parse %s.", s);
    return x;
  }
}
